<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Document</title>
  <style>
    .expressContainer {
      /* max-width: 100%; */
      /* width: 800px; */
      width: 100%;
      height: 800px;
      display: flex;
      flex-wrap: wrap;
      border: 1px solid grey;
    }

    .meetingList {
      min-width: 320px;
      width: 40%;
      height: 100%;
      display: flex;
      flex-direction: column;
    }

    .meetingRoom {
      display: flex;
      flex-direction: column;
      background-color: lightgreen;
      width: 100%;
      height: 50%;
      border: 1px solid grey;
      box-sizing: border-box;
    }

    .world {
      display: flex;
      flex-direction: column;
      background-color: pink;
      width: 60%;
      height: 100%;
      min-width: 480px;
      border: 1px solid grey;
      box-sizing: border-box;
    }

    .operation {
      background-color: lightseagreen;
    }

    .worldTalkArea {
      /* display: flex; */
      height: 100%;
      background-color: lightskyblue;
    }

    .person {
      /* margin: 5px;
      width: 150px;
      height: 150px;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center; */
      margin: 5px;
      border: 2px solid #000;
      padding: 5px;
      display: inline-block;
      border-radius: 5px;
    }

    /* .person[username="robot_*"] {
      display: none;
    } */

    .person .camera {
      width: 66.6%;
      height: 66.6%;
      border-radius: 50%;
      object-fit: cover;
      background-color: lightcyan;
    }

    .person .axis {
      width: 30px;
      border: none;
    }

    .person .btn {
      border: none;
      padding: 5px;
      cursor: pointer;
    }

    .meetingTalkArea {
      flex-direction: column;
      display: flex;
      height: 100%;
    }

    .meetingTalkArea .screenStreamList,
    .meetingTalkArea .videoStreamList {
      display: flex;
    }

    .meetingRoom .person {
      width: 120px;
      height: 120px;
    }

    .meetingRoom .person .camera {
      width: 80%;
      height: 80%;
    }


    [v-cloak] {
      display: none;
    }
  </style>
</head>

<body>
  <div id="app">
    <div class="container" v-cloak>
      <div class="info">
        <span>userID:{{myUserID}}</span>
        <br>
        <span v-if="!isRobot">
          <!-- <span>userID:{{myUserID}}</span> -->
          <lable>UserName</lable><input v-model="myUserName">
        </span>
        <button v-on:click="enterRoom(this.worldRoomID, 'world')" :disabled="loginLoading">进入世界</button>
        <button @click="zegoLogout">退出世界</button>
      </div>
      <div class="expressContainer">
        <!-- 会议室 -->
        <div class="meetingList" style="display: none;">
          <div class="meetingRoom" v-for="(meetingRoomID,index) in meetingRooms">
            <div class="operation">
              <span>roomID:{{meetingRoomID}}</span><br />
              <button v-on:click="enterRoom(meetingRoomID, 'meeting')" :disabled="loginLoading">进入会议室</button>
              <button v-on:click="startScreenShare(meetingRoomID)">开始屏幕共享</button>
            </div>
            <div class="meetingTalkArea">
              <template v-if="!isWorld && roomID === meetingRoomID">
                <div class="screenStreamList">
                  <div class="person" v-for="item in screenStreams" :key="item.userID">
                    <video :src-object.camel.prop="item.stream" class="camera" autoplay muted controls></video>
                    <div>屏幕共享流</div>
                  </div>
                </div>
                <div class="videoStreamList">
                  <div class="person" v-for="item in userMap" :key="item.userID">
                    <video :src-object.camel.prop="item.videoStream" class="camera" autoplay muted controls></video>
                    <div>{{item.userName}}</div>
                  </div>
                </div>
              </template>
            </div>
          </div>
        </div>
        <!-- 世界 -->
        <div class="world" style="width: 100%;">
          <div class="operation">
            <span>roomID: {{worldRoomID}}</span>
            <br />
            <section>
              <h3>当前在线人数：<small>{{onlineCount}}</small></h3>
              <!-- <h3>正在说话的人：</h3>
              <p>
                <span v-for="item in sayMan">
                  {{item}}
                </span>
              </p> -->
            </section>

          </div>
          <div class="worldTalkArea" id="worldTalkArea" style="display:block !important">
            <template v-if="isWorld">
              <span v-for="item in userMap" :key="item.userID" :userid="item.userID">


                <div class="person" v-if="item.userName.indexOf('robot')== -1"
                  :style="{borderColor: item.soundLevel? '#0f0': '000'}">
                  <!-- <video :src-object.camel.prop="item.videoStream" class="camera" autoplay muted controls></video> -->
                  <div>{{item.userName}}</div>
                  <button v-if="!micState" :disabled="item.userName != myUserName"
                    v-on:click="enableMic()">开启麦克风</button>
                  <button v-if="micState" :disabled="item.userName != myUserName"
                    v-on:click="enableMic()">关闭麦克风</button>
                  <div style="display: none;">
                    <label>x:<input class="axis" type="text" v-model.number="item.x"
                        :disabled="item.userID !== myUserID" /></label>
                    <label>y:<input class="axis" type="text" v-model.number="item.y"
                        :disabled="item.userID !== myUserID" /></label>
                    <button v-if="item.userID === myUserID" v-on:click="updateMyPosi(item.x,item.y)"
                      class="btn">确定</button>
                  </div>
                </div>
              </span>
            </template>
          </div>
        </div>
      </div>
    </div>
  </div>
  <script src="https://unpkg.com/vue@3.2.37/dist/vue.global.js"></script>
  <script src="./lib/ZegoExpressWebRTC-2.18.0.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

  <script>
    const appid = 1802444573
    const server = "wss://webliveroom1802444573-api.imzego.com/ws"
    let zg, rangeAudio;
    document.addEventListener("DOMContentLoaded", () => {
      zg = new ZegoExpressEngine(appid, server)
      rangeAudio = zg.createRangeAudioInstance()
      zg.setDebugVerbose(false)
      zg.setSoundLevelDelegate(true, 1000); //开启声浪
      zg.setLogConfig({
        logLevel: "error"
      })

      const ROOMSTATE = {
        DISCONNECTED: 'DISCONNECTED',
        CONNECTING: 'CONNECTING',
        CONNECTED: 'CONNECTED',
      }
      const UPDATE_TYPE = {
        DELETE: "DELETE",
        UPDATE: "UPDATE",
        ADD: "ADD"
      }
      const app = {
        data() {
          return {
            roomID: 0,
            roomType: "world", // "world" or "meeting"
            roomState: "", // 'DISCONNECTED' | 'CONNECTING' | 'CONNECTED'
            myUserID: "",
            myUserName: "",
            videoStreamID: "",
            screenStreamID: "",
            worldRoomID: "classroom_zego_3721",
            // vue3 可以用动态属性来渲染列表，但是在 vue2 无法直接监听新增的对象。要用 $set 才行。
            userMap: {
              // userID: {
              //   userID,
              //   userName,
              //   x,
              //   y,
              //   videoStream,
              //   screenStream,
              // }
            },
            zegoCallBacks: {}, // 全局唯一的 zego 回调，这种形式下，同一个回调注册多次也只会调用一次。
            loginLoading: false, // 登录中
            onlineCount: 0,
            isRobot: false,
            micState: false,
            // sayMan: []
          }
        },
        computed: {
          isWorld() {
            return this.roomType === "world"
          },
          // 会议室列表
          meetingRooms() {
            return Array(2).fill(0).map((item, index) => item = String(Number(this.worldRoomID) + index + 1))
          },
          // 屏幕共享流列表
          screenStreams() {
            const screenStreams = []
            for (const key in this.userMap) {
              if (Object.hasOwnProperty.call(this.userMap, key)) {
                const user = this.userMap[key];
                if (user.screenStream) {
                  screenStreams.push({
                    stream: user.screenStream,
                    userID: user.userID
                  })
                }
              }
            }
            console.log('mytag screenStreams', screenStreams);
            return screenStreams
          }
        },
        async mounted() {
          this.zegoCallBacks = this.zegoCallBack();
          // 添加机器人逻辑
          if (/robot/.test(location.search)) {
            isRobot = true;
            //进入世界
            this.enterRoom(this.worldRoomID, 'world');
          }
        },
        methods: {
          async enterRoom(roomID, roomType) {
            try {
              this.loginLoading = true
              if (!!this.roomID) this.zegoLogout()

              const random4 = Math.random().toString().slice(2)
              this.roomID = roomID
              this.roomType = roomType
              // this.myUserID = !!this.myUserID ? this.myUserID : "meeting_" + this.roomID + "_uesrID_" + random4
              // this.myUserName = !!this.myUserName ? this.myUserName : "name_" + random4
              // this.videoStreamID = !!this.videoStreamID ? this.videoStreamID : "meeting_" + this.roomID + "_videoStream_" + random4
              // this.screenStreamID = !!this.screenStreamID ? this.screenStreamID : "meeting_" + this.roomID + "_screenStream_" + random4

              this.myUserID = !!this.myUserID ? this.myUserID : "meeting-" + this.roomID + "-uesrID-" + random4
              this.myUserName = !!this.myUserName ? this.myUserName : "robot_name_" + random4
              this.videoStreamID = !!this.videoStreamID ? this.videoStreamID : "meeting_" + this.roomID +
                "_videoStream_" + random4
              this.screenStreamID = !!this.screenStreamID ? this.screenStreamID : "meeting_" + this.roomID +
                "_screenStream_" + random4

              // 注册回调
              this.regListener()
              // 设备能力检测
              await this.compatiblityTest()
              // 登录
              await this.zegoLogin()
              // 推流音频设置
              await this.setAudioRangeConfig()
              // 推流视频设置，只在 会议室 中推视频流。
              await this.setVideoConfig(this.isWorld ? false : true, this.videoStreamID)

              this.updateUserMap(this.myUserID, {
                userID: this.myUserID,
                userName: this.myUserName,
                x: 0,
                y: 0,
              })
              this.loginLoading = false
            } catch (error) {
              this.loginLoading = false
              console.error("loginRoom error", error)
            }
          },
          async zegoLogin() {
            const token = await this.getToken04(this.myUserID)
            const res = await zg.loginRoom(
              this.roomID,
              token, {
                userID: this.myUserID,
                userName: this.myUserName
              }, {
                userUpdate: true
              }
            )
          },
          async setAudioRangeConfig() {
            // 设置接受范围
            rangeAudio.setAudioReceiveRange(100)
            // 更新位置
            await this.updateMyPosi(0, 0)
            // 3D 音效
            rangeAudio.enableSpatializer(true)
            // 开启麦克风 -- 即推音频流
            // rangeAudio.enableMicrophone(true)
            // 开启扬声器 -- 即自动拉流
            rangeAudio.enableSpeaker(true)
          },
          // 设置是否 推视频流
          async setVideoConfig(needVideoStream = true) {
            if (needVideoStream) {
              // const videoStream = await this.createVideoStream()
              this.updateUserMap(this.myUserID, {
                videoStream
              })
              const res = zg.startPublishingStream(this.videoStreamID, videoStream)
              return videoStream
            }
          },
          enableMic() {
            this.micState = !this.micState;
            rangeAudio.enableMicrophone(this.micState);
            this.updateUserMap(this.myUserID, {
              soundLevel: 0
            })
          },
          // 流创建
          async createVideoStream() {
            const videoStream = await zg.createStream({
              camera: {
                video: true,
                audio: false,
                videoQuality: 1
              }
            })
            return videoStream
          },
          // 推屏幕共享流 
          async startScreenShare(roomID) {
            if (this.roomID !== roomID || this.roomState !== ROOMSTATE.CONNECTED) return console.error(
              '尚未登录该房间');
            const screenStream = await this.createScreenStream()
            this.updateUserMap(this.myUserID, {
              screenStream
            })
            const res = zg.startPublishingStream(this.screenStreamID, screenStream)
            return screenStream
          },
          // 屏幕共享流创建
          async createScreenStream() {
            const screenStream = await zg.createStream({
              screen: true
            })
            return screenStream
          },
          async zegoLogout() {
            // 停止推流 和 销毁流
            const myUserInfo = this.userMap[this.myUserID]
            zg.stopPublishingStream(this.videoStreamID)
            zg.stopPublishingStream(this.screenStreamID)
            if (myUserInfo.videoStream) zg.destroyStream(myUserInfo.videoStream)
            if (myUserInfo.screenStream) zg.destroyStream(myUserInfo.screenStream)
            // 登出
            zg.logoutRoom()
            // 注销回调
            this.cancelListener()
            // 反初始化
            this.roomState = ""
            this.userMap = {}
          },

          async updateMyPosi(x, y) {
            console.log("mytag x,y", x, y)
            const position = [x, y, 0]
            const axisForward = [1, 0, 0]
            const axisRight = [0, 1, 0]
            const axisUp = [0, 0, 1]
            rangeAudio.updateSelfPosition(position, axisForward, axisRight, axisUp)
            const res = await zg.sendBroadcastMessage(this.roomID, `${x},${y}`)
            console.log("mytag res", res)
          },

          // 更新用户位置
          updateOthersAudioSource(userID, x, y) {
            console.log('mytag updateAudioSource userID, x, y', userID, x, y);
            x = Number(x)
            y = Number(y)
            if (isNaN(x) || isNaN(y)) return console.error('位置信息错误', x, y);
            const res = rangeAudio.updateAudioSource(userID, [x, y, 0])
          },

          async compatiblityTest() {
            // const checkRes = await zg.checkSystemRequirements()
            // console.log("mytag checkRes", JSON.stringify(checkRes))
            const enumRes = await zg.enumDevices()
            // console.log("mytag enumRes", JSON.stringify(enumRes))
            const isSupport = rangeAudio.isAudioContextRunning()
            console.log("mytag isSupport", isSupport)
            // const result = await rangeAudio.resumeAudioContext();
          },

          updateUserMap(userID, userProps, updateType) {
            // 用户删除
            if (updateType === UPDATE_TYPE.DELETE) {
              if (!userID) return console.error('需要删除的 userID 不存在', );
              this.userMap[userID] = {}
              delete this.userMap[userID]
              console.log('mytag userID', userID);
              console.log('mytag this.userMap', this.userMap);
              return
            }

            // 用户更新
            if (this.userMap[userID] instanceof Object) {
              for (const key in userProps) {
                if (Object.hasOwnProperty.call(userProps, key)) {
                  this.userMap[userID][key] = userProps[key];
                }
              }
            } else {
              // 用户新增
              this.userMap[userID] = {}
              for (const key in userProps) {
                if (Object.hasOwnProperty.call(userProps, key)) {
                  this.userMap[userID][key] = userProps[key];
                }
              }
            }
          },
          zegoCallBack() {
            return {
              // 用户更新
              roomUserUpdateCallBack: (roomID, updateType, userList) => {
                console.log('mytag roomUserUpdateCallBack roomID, updateType, userList', roomID, updateType,
                  userList);
                try {
                  if (updateType === "ADD") {
                    userList.forEach((updatedUser) => {
                      const {
                        userID,
                        userName
                      } = updatedUser
                      this.updateUserMap(userID, {
                        userID,
                        userName,
                        x: 0,
                        y: 0
                      })
                      this.updateOthersAudioSource(userID, 0, 0)
                    })
                  } else {
                    userList.forEach(updatedUser => {
                      this.updateUserMap(updatedUser.userID, {}, UPDATE_TYPE.DELETE)
                    })
                  }
                } catch (error) {
                  console.error("roomUserUpdateCallBack error", error)
                }
              },
              //用户在线人数更新
              roomOnlineUserCountUpdateCallBack: (roomID, count) => {
                this.onlineCount = count;
              },
              // 推拉流音浪回调,该接口目前不兼容safari
              soundLevelUpdateCallBack: (soundLevelList) => {
                // let sayMan = [];
                soundLevelList.forEach(item => {
                  const userID = item.streamID.split('_')[0];
                  if (item.soundLevel > 3) {
                    // let userName = document.querySelector(`[userid=${userID}]`).querySelector('div').textContent;
                    // sayMan.push(userName);
                    this.updateUserMap(userID, {
                      soundLevel: item.soundLevel
                    });
                  } else {
                    this.updateUserMap(userID, {
                      soundLevel: 0
                    });
                  }
                });
                // this.sayMan = sayMan;
              },
              // 流更新
              roomStreamUpdateCallBack: async (roomID, updateType, streamList, extendedData) => {
                try {
                  console.log('mytag roomID, updateType, streamList,extendedData', roomID, updateType,
                    streamList, extendedData);
                  // 需要在循环内使用 await 需要用 forof 或者 forin 
                  for (const streamItem of streamList) {
                    const {
                      streamID,
                      user: streamUser,
                      extraInfo
                    } = streamItem
                    const {
                      userID,
                      userName
                    } = streamUser
                    const isVideoStream = streamID.includes("videoStream")
                    const isScreenStream = streamID.includes("screenStream")
                    // 范围语音的音频流规则是 streamId = userID， 这里只要拉自己需要的流即可
                    if (isVideoStream || isScreenStream) {
                      if (updateType === "ADD") {
                        const stream = await zg.startPlayingStream(streamID)
                        const query = {
                          userID,
                          userName
                        }
                        isVideoStream ? query.videoStream = stream : query.screenStream = stream
                        this.updateUserMap(userID, query)
                      } else {
                        const query = {}
                        isVideoStream ? query.videoStream = null : query.screenStream = null
                        query.soundLevel = 0;
                        this.updateUserMap(userID, query)
                        zg.stopPlayingStream(streamID)
                      }
                    }
                  }

                } catch (error) {
                  console.error("roomStreamUpdateCallBack error", error)
                }
              },
              // 房间状态回调
              roomStateUpdateCallBack: (roomID, state, errorCode, extendedData) => {
                this.roomState = state
              },
              roomStateChangedCallBack: (roomID, reason) => {
                if (reason == 'RECONNECT_FAILED') {
                  console.log('RECONNECT_FAILED');
                  this.enterRoom(this.worldRoomID, 'world');
                }
              },
              // 推流质量
              publishQualityUpdateCallBack: (streamID, state) => {
                // console.log("mytag publishQualityUpdate streamID,state", streamID, state)
              },
              // 推流状态
              publisherStateUpdateCallBack: (result) => {
                console.log("mytag publisherStateUpdate result", result)
              },
              // 拉流状态
              playerStateUpdateCallBack: (result) => {
                console.log("mytag playerStateUpdate result", result)
              },
              // 房间附加消息
              roomExtraInfoUpdateCallBack: (roomID, roomExtraInfoList) => {
                console.log("mytag roomExtraInfoUpdateCallBack", roomID, roomExtraInfoList)
              },
              // 广播消息
              IMRecvBroadcastMessageCallBack: (roomID, chatDataList) => {
                try {
                  chatDataList.forEach((chatData) => {
                    console.log("mytag IMRecvBroadcastMessageCallBack", roomID, chatData)
                    const {
                      fromUser,
                      message
                    } = chatData
                    const x = message.split(",")[0]
                    const y = message.split(",")[1]
                    const {
                      userID,
                      userName
                    } = fromUser
                    // 更新用户信息
                    this.updateUserMap(fromUser.userID, {
                      userID,
                      userName,
                      x,
                      y
                    })
                    // 更新音源位置
                    this.updateOthersAudioSource(fromUser.userID, x, y)
                  })
                } catch (error) {
                  console.error("error", error)
                }
              },
              // 麦克风状态
              microphoneStateUpdateCallBack: (state, errorCode, extendedData) => {
                console.log("mytag state, errorCode, extendedData", state, errorCode, extendedData)
                if (state === 0) {
                  // 关闭麦克风声音
                } else if (state === 1) {
                  // 开启麦克风中
                } else if (state === 2) {
                  // 打开麦克风发送声音
                }
              }
            }
          },
          cancelListener() {
            zg.off("roomUserUpdate", this.zegoCallBacks.roomUserUpdateCallBack)
            zg.off("roomOnlineUserCountUpdate", this.zegoCallBacks.roomOnlineUserCountUpdateCallBack)
            zg.off("soundLevelUpdate", this.zegoCallBacks.soundLevelUpdateCallBack)
            zg.off("roomStreamUpdate", this.zegoCallBacks.roomStreamUpdateCallBack)
            zg.off("roomStateUpdate", this.zegoCallBacks.roomStateUpdateCallBack)
            zg.off("roomStateChanged", this.zegoCallBacks.roomStateChangedCallBack)
            zg.off("publishQualityUpdate", this.zegoCallBacks.publishQualityUpdateCallBack)
            zg.off("publisherStateUpdate", this.zegoCallBacks.publisherStateUpdateCallBack)
            zg.off("playerStateUpdate", this.zegoCallBacks.playerStateUpdateCallBack)
            zg.off("roomExtraInfoUpdate", this.zegoCallBacks.roomExtraInfoUpdateCallBack)
            zg.off("IMRecvBroadcastMessage", this.zegoCallBacks.IMRecvBroadcastMessageCallBack)
            rangeAudio.off("microphoneStateUpdate", this.zegoCallBacks.microphoneStateUpdateCallBack)
          },
          regListener() {
            zg.on("roomUserUpdate", this.zegoCallBacks.roomUserUpdateCallBack)
            zg.on("roomOnlineUserCountUpdate", this.zegoCallBacks.roomOnlineUserCountUpdateCallBack)
            zg.on("soundLevelUpdate", this.zegoCallBacks.soundLevelUpdateCallBack)
            zg.on("roomStateUpdate", this.zegoCallBacks.roomStateUpdateCallBack)
            zg.on("roomStateChanged", this.zegoCallBacks.roomStateChangedCallBack)
            zg.on("publishQualityUpdate", this.zegoCallBacks.publishQualityUpdateCallBack)
            zg.on("publisherStateUpdate", this.zegoCallBacks.publisherStateUpdateCallBack)
            zg.on("playerStateUpdate", this.zegoCallBacks.playerStateUpdateCallBack)
            zg.on("roomExtraInfoUpdate", this.zegoCallBacks.roomExtraInfoUpdateCallBack)
            zg.on("IMRecvBroadcastMessage", this.zegoCallBacks.IMRecvBroadcastMessageCallBack)
            rangeAudio.on("microphoneStateUpdate", this.zegoCallBacks.microphoneStateUpdateCallBack)
          },

    
          getToken04(
            idName
          ) {
            return new Promise((res, rej) => {
              // const tokenUrl = "/getToken";
              const tokenUrl = "https://sig-liveroom-admin.zego.cloud/thirdToken/get";
              axios
                .post(tokenUrl, {
                  version: "04",
                  appid,
                  idName,
                  "roomId":"classroom_zego_3721",
                  privilege: {
                    "1": 1,
                    "2": 1
                  }
                })
                .then((result) => {
                  res(result.data.data.token);
                });
            });
          }
          //  function get04Token(
          //   app_id: number,
          //   id_name: string
          // ): Promise<string> {
          //   return new Promise((res, rej) => {
          //     const tokenUrl = "http://192.168.53.98:8081/token04";
          //     axios
          //       .get(tokenUrl, {
          //         params: {
          //           app_id,
          //           id_name
          //           // cgi_token: generateTokenInfo(app_id)
          //         }
          //       })
          //       .then((result: any) => {
          //         console.log(result);
          //         res(result.data);
          //       });
          //   });
          // }
        }
      }
      Vue.createApp(app).mount('#app')
    })
  </script>
</body>

</html>